

local service = require "ddcl.service"
local socket = require "ddcl.socket"
local tcp_channel = require "ddcl.tcp_channel"
local inspect = require "ddcl.inspect"

local function create_channel(M, fd)
    M.c = tcp_channel(fd, {
        forward = M.forward,
        close_fn = function(c)
            for i, v in ipairs(M.send_queue) do
                local co = v
                service.fork(function()
                    service.resume(co, false, "closed")
                end)
            end
            M.send_queue = {}
            if M.on_close then
                M.on_close(M)
            end
        end
    })
end

local function connect_host(M)
    if not M.host then
        return false
    end
    M.connecting = true
    local ret, fd = pcall(socket.connect, M.host, M.port)
    M.connecting = false
    assert(ret, fd)
    create_channel(M, fd)
    if M.on_connect then
        M.on_connect(M)
    end
    M.connecting = false
    M.connected = true
    for i, v in ipairs(M.send_queue) do
        local co = v
        service.fork(function()
            service.resume(co, true)
        end)
    end
    M.send_queue = {}
    return true
end

local function create_request(opt)
    local M = {
        c = nil,
        host = opt.host,
        port = opt.port,
        send_queue = {},
        connecting = false,
        connected = false,
        forward = opt.forward,
        on_close = nil,
        on_connect = nil,
    }

    if opt.fd then
        create_channel(M, opt.fd)
    end

    function M.connect()
        assert(connect_host(M), "connect error")
    end

    function M.close()
        if M.c then
            M.c.close()
        end
        M.connected = false
        M.connecting = false
        M.c = nil
    end

    return M
end

local function send_request(M, data)
    if not M.c then
        if M.connecting then
            table.insert(M.send_queue, service.running())
            assert(service.yield())
        else
            assert(connect_host(M), "connect error")
        end
    end
    M.c.send(data)
end

local function send_request_raw(M, p, sz)
    if not M.c then
        if M.connecting then
            table.insert(M.send_queue, service.running())
            assert(service.yield())
        else
            assert(connect_host(M), "connect error")
        end
    end
    M.c.sendraw(p, sz)
end

local function send_request_con(M, fn, ...)
    if not M.c then
        if M.connecting then
            table.insert(M.send_queue, service.running())
            assert(service.yield())
        else
            assert(connect_host(M), "connect error")
        end
    end
    fn(M.c, ...)
end

local function queue_request(opt)
    local M = create_request(opt)
    M.on_close = function()
        M.c = nil
        M.connected = false
        M.connecting = false
        if opt.on_close then
            opt.on_close(M)
        end
    end

    M.on_connect = function()
        if opt.on_connect then
            opt.on_connect(M, M.c)
        end
    end
    M.queue = {}
    M.running = false

    function M.request(data, response_fn)
        assert(response_fn)
        coroutine.running()
        send_request(M, data)
        if M.running then
            table.insert(M.queue, service.running())
            service.yield()
        end
        M.running = true
        local ret, data = pcall(response_fn, M, M.c)
        if #M.queue > 0 then
            local co = table.remove(M.queue, 1)
            service.fork(function()
                service.resume(co)
            end)
        else
            M.running = false
        end
        if not ret then
            M.close()
        end
        assert(ret, data)
        return data
    end
    return M
end


local function session_request(opt)
    local response_fn = opt.response_fn
    assert(response_fn)
    local M = create_request(opt)
    M.ss_map = {}
    M.on_close = function()
        M.c = nil
        M.connected = false
        M.connecting = false
        for k, v in pairs(M.ss_map) do
            service.fork(function()
                service.resume(v, nil, "closed")
            end)
        end
        M.ss_map = {}
        if opt.on_close then
            opt.on_close(M)
        end
    end

    local function while_read()
        while M.c do
            local ret, session, data = xpcall(response_fn, print, M, M.c)
            if not ret  then
                M.close()
                return
            end
            local co = M.ss_map[session]
            if not co then
                M.close()
                return
            end
            M.ss_map[session] = nil
            service.fork(function()
                service.resume(co, data)
            end)
        end
    end
    M.on_connect = function()
        if opt.on_connect then
            opt.on_connect(M, M.c)
        end
        service.fork(function()
            while_read()
        end)
    end

    function M.request(data, session)
        send_request(M, data)
        M.ss_map[session] = service.running()
        local rsp, err = service.yield()
        assert(rsp, err)
        return rsp
    end

    function M.request_raw(p, sz, session)
        send_request_raw(M, p, sz)
        M.ss_map[session] = service.running()
        local rsp, err = service.yield()
        assert(rsp, err)
        return rsp
    end

    function M.request_con(fn, session, ...)
        send_request_con(M, fn, ...)
        M.ss_map[session] = service.running()
        local rsp, err = service.yield()
        assert(rsp, err)
        return rsp
    end

    return M
end



return {
    queue = queue_request,
    session = session_request,
}
